from wormbane.direction import *
from wormbane.Feature import *
from wormbane.geometry import *
from wormbane.Level import Level
from wormbane.Pathfinder import Pathfinder
from wormbane.Room import Room
import random


MIN_SIZE = Vector(6, 6)


def random_pop(lst, random=random):
    index = random.randrange(len(lst))
    lst[index], lst[-1] = lst[-1], lst[index]
    return lst.pop()


def replace_nth(iterable, n, replacement):
    for i, value in enumerate(iterable):
        yield replacement if i == n else value


def split_box(box, axis, pos):
    if not box.min[axis] <= pos <= box.max[axis]:
        raise ValueError('split position out of range')

    yield Box(box.min, replace_nth(box.max, axis, pos))
    yield Box(replace_nth(box.min, axis, pos), box.max)


def divide_box(box, min_size, max_count, random=random):

    def max_split_axis(box):
        splits = [(a, axis) for axis, a in enumerate(box.size)
                  if a >= 2 * min_size[axis]]
        return max(splits)[1] if splits else -1

    def random_split_pos(box, axis):
        return random.randrange(box.min[axis] + min_size[axis],
                                box.max[axis] - min_size[axis] + 1)

    axes = range(len(min_size))
    queue = [box]
    result = []
    while queue and len(queue) + len(result) < max_count:
        box = random_pop(queue, random)
        axis = max_split_axis(box)
        if axis == -1:
            result.append(box)
        else:
            pos = random_split_pos(box, axis)
            queue.extend(split_box(box, axis, pos))
    result.extend(queue)
    return result


def random_floor_tile(level, room, random=random):
    x = random.randrange(room.bounds.min.x + 1, room.bounds.max.x - 1)
    y = random.randrange(room.bounds.min.y + 1, room.bounds.max.y - 1)
    return level.tiles[x][y]


def generate_rooms(level, random=random):

    def random_size(min_size, max_size, area_size):
        return Vector(random.randrange(a, min(b, c) + 1)
                      for a, b, c in zip(min_size, max_size, area_size))

    def random_pos(size, area):
        return Vector(random.randrange(area.min.x, area.max.x - size.x + 1),
                      random.randrange(area.min.y, area.max.y - size.y + 1))

    min_size = Vector(6, 6)
    max_size = Vector(18, 12)
    padding = Vector(3, 3)
    padded_areas = divide_box(Box(level.bounds.min, level.bounds.max
                                  + padding),
                              min_size + padding, random.randrange(6, 10),
                              random)
    areas = [Box(area.min, area.max - padding) for area in padded_areas]
    for area in areas:
        size = random_size(min_size, max_size, area.size)
        pos = random_pos(size, area)
        yield Room(Box(pos, pos + size))


def generate_corridors(level, random=random):

    # Create a pathfinder.
    find_path = Pathfinder(level.size)

    def find_closest_rooms(rooms_a, rooms_b):
        best_dist = None
        best_room_a = None
        best_room_b = None
        for room_a in rooms_a:
            for room_b in rooms_b:
                if room_a is not room_b:
                    dist = squared_distance(room_a.bounds, room_b.bounds)
                    if best_dist is None or dist < best_dist:
                        best_dist = dist
                        best_room_a = room_a
                        best_room_b = room_b
        return best_room_a, best_room_b

    def connect_rooms(room_a, room_b):
        start_pos = random_floor_tile(level, room_b, random).pos
        goal_pos = random_floor_tile(level, room_a, random).pos

        def is_corner(tile):
            return (type(tile.feature) is Wall
                    and is_diagonal_direction(tile.feature.dir))

        def is_border(tile):
            return (type(tile.feature) is Wall and tile.feature.dir
                    or type(tile.feature) is Door)

        def is_beside_door(tile):
            if type(tile.feature) is Wall and tile.feature.dir:
                for dir in ('north', 'west', 'east', 'south'):
                    dx, dy = direction_to_planar_vector(dir)
                    x, y = tile.pos.x + dx, tile.pos.y + dy
                    try:
                        neighbor_tile = level.tiles[x][y]
                    except IndexError:
                        pass
                    else:
                        if type(neighbor_tile.feature) is Door:
                            return True
            return False

        def neighbors(pos):
            tile = level.tiles[pos.x][pos.y]
            for x, y in ((pos.x, pos.y + 1), (pos.x - 1, pos.y),
                         (pos.x + 1, pos.y), (pos.x, pos.y - 1)):
                try:
                    neighbor_tile = level.tiles[x][y]
                    if (not is_corner(neighbor_tile)
                        and not is_beside_door(neighbor_tile)
                        and not (is_border(tile)
                                 and is_border(neighbor_tile))):
                        yield neighbor_tile.pos
                except IndexError:
                    pass

        def cost(a, b):
            return 1

        def heuristic(pos):
            return manhattan_distance(pos, goal_pos)

        path = find_path(start_pos, goal_pos, neighbors, cost, heuristic)
        for pos in path:
            tile = level.tiles[pos.x][pos.y]
            if type(tile.feature) is Wall:
                if tile.feature.dir:
                    if random.randrange(2):
                        tile.feature = Door(dir=tile.feature.dir)
                        tile.feature.closed = bool(random.randrange(2))
                        if tile.feature.closed and random.randrange(2):
                            tile.feature.locked = True
                    else:
                        if random.randrange(2):
                            tile.feature.secret = True
                        else:
                            tile.feature = Doorway(dir=tile.feature.dir)
                else:
                    if random.randrange(100) < 2:
                        tile.feature = Wall()
                        tile.feature.secret = True
                    else:
                        tile.feature = Corridor()
            elif type(tile.feature) is Corridor:
                break

    # Track unconnected and connected rooms.
    unconnected_rooms = set(level.rooms)
    connected_rooms = set()

    # Start by connecting the two closest rooms.
    room_a, room_b = find_closest_rooms(unconnected_rooms, unconnected_rooms)
    unconnected_rooms.remove(room_a)
    unconnected_rooms.remove(room_b)
    connect_rooms(room_a, room_b)
    connected_rooms.add(room_a)
    connected_rooms.add(room_b)

    # Connect the remaining rooms.
    while unconnected_rooms:
        room_a, room_b = find_closest_rooms(unconnected_rooms, connected_rooms)
        unconnected_rooms.remove(room_a)
        connect_rooms(room_a, room_b)
        connected_rooms.add(room_a)
        

def paint_room(room, level, lit):

    # boundaries
    north = room.bounds.max.y
    west = room.bounds.min.x
    east = room.bounds.max.x
    south = room.bounds.min.y

    # corners
    level.tiles[west][north - 1].feature = Wall('northwest')
    level.tiles[east - 1][north - 1].feature = Wall('northeast')
    level.tiles[west][south].feature = Wall('southwest')
    level.tiles[east - 1][south].feature = Wall('southeast')

    # walls
    for x in xrange(west + 1, east - 1):
        level.tiles[x][north - 1].feature = Wall('north')
    for y in xrange(south + 1, north - 1):
        level.tiles[west][y].feature = Wall('west')
    for y in xrange(south + 1, north - 1):
        level.tiles[east - 1][y].feature = Wall('east')
    for x in xrange(west + 1, east - 1):
        level.tiles[x][south].feature = Wall('south')

    # floor
    for x in xrange(west + 1, east - 1):
        for y in xrange(south + 1, north - 1):
            level.tiles[x][y].feature = Floor()

    # room and light
    for x in xrange(west, east):
        for y in xrange(south, north):
            level.tiles[x][y].room = room
            level.tiles[x][y].lit = lit


def generate_stairs(level, random=random):

    rooms = list(level.rooms)

    up_room = random_pop(rooms)
    up_tile = random_floor_tile(level, up_room, random)
    up_tile.feature = Stair(dir='up')
    level.stair_up_tile = up_tile

    down_room = random_pop(rooms)
    down_tile = random_floor_tile(level, down_room, random)
    down_tile.feature = Stair(dir='down')
    level.stair_down_tile = down_tile


def generate_level(random=random):
    level = Level()
    level.rooms = list(generate_rooms(level, random))
    for room in level.rooms:
        paint_room(room, level, bool(random.randrange(2)))
    generate_corridors(level, random)
    generate_stairs(level, random)
    return level


def test():
    generate_level()


if __name__ == '__main__':
    test()
